export default class FormulaEditorCore {
  // 编辑器实例
  editor = null
  // 公式对象列表
  formulaObjList = []
  // 公式文本
  text = ''
  // 公式标记
  marks = []
  // 监听方法
  listen = ['change', 'inputRead', 'beforeChange']

  constructor(editor, text = '', formulaObjList = []) {
    this.editor = editor
    this.text = text
    this.formulaObjList = formulaObjList
  }

  // 获取公式列表
  getFormulaList() {
    return this.formulaObjList.flatMap(o => o.formulaList)
  }

  /**
   * 代码数据回显
   * @param formulaData
   */
  renderData(formulaData) {
    if (Object.keys(formulaData).length === 0) return
    this.editor.setValue(formulaData.text)
    this.marks = formulaData.marks
    this.marks.forEach(mark => {
      // 字段/变量名、字段/变量值的高亮不一样
      let className = ''
      if (mark.markType === 'fieldName') {
        className = 'cm-field-name'
      } else if (mark.markType === 'fieldValue') {
        className = 'cm-field-value'
      }
      // 参考手册 https://codemirror.net/5/doc/manual.html#api_marker
      this.editor.doc.markText(mark.from, mark.to, {
        className: className, attributes: {
          'data-fieldCode': mark.fieldCode,
          'data-fieldPath': mark.fieldPath,
          'data-markType': mark.markType
        }, // 鼠标把该标签当做一个整体、不可以点击到中间，删除时一起删除
        atomic: true,
      })
    })
  }

  /**
   * 初始化时注册监听器
   */
  registerListen() {
    for (const item of this.listen) {
      // 监听 ['change', 'inputRead', 'beforeChange'] 事件
      const fn = this[`on${item.charAt(0).toUpperCase()}${item.slice(1)}`]
      this.editor.on(item, fn.bind(this, ...arguments))
    }
  }

  /**
   * 当输入时触发
   * @param cm
   * @param change
   */
  onInputRead(cm, change) {
    cm.showHint({
      hint: this.customHint.bind(this), completeSingle: false,
    })
  }

  /**
   * 当编辑器中文本内容改变
   * @param cm 编辑器示例
   * @param changeObj 改变的数据对象
   */
  onChange(cm, changeObj) {
    this.marks = cm
      .getAllMarks()
      .filter(o => o.className === 'cm-field-name' || o.className === 'cm-field-value')
      .map(marks => {
        const { attributes } = marks
        return {
          ...marks.find(),
          fieldCode: attributes['data-fieldCode'],
          markType: attributes['data-markType'],
          fieldPath: attributes['data-fieldPath']
        }
      })
    this.text = cm.getValue()
    console.log('编辑器代码改变，当前为 ', this.text)
    if (changeObj.origin === 'complete') {
      // 向左移动一个字符
      this.moveCursor('left', 1)
    }
  }

  /**
   * 编辑器代码改变前，用于编辑器按钮自动插入时使用
   * @param cm
   * @param changeObj
   */
  onBeforeChange(cm, changeObj) {
    const { text, from, cancel } = changeObj
    const data = this.matchField(text[0])
    if (data.length) {
      cancel()
      const field = JSON.parse(data[0])
      const fieldFrom = { ...from }
      const to = { ...from, ch: from.ch + field.fieldName.length }
      cm.replaceRange(field.fieldName, fieldFrom)
      // 字段/变量名、字段/变量值的高亮不一样
      let className = ''
      if (field.markType === 'fieldName') {
        className = 'cm-field-name'
      } else if (field.markType === 'fieldValue') {
        className = 'cm-field-value'
      }
      cm.doc.markText(fieldFrom, to, {
        className: className, attributes: {
          'data-fieldCode': field.fieldCode,
          'data-markType': field.markType,
          'data-fieldPath': field.fieldPath
        }, atomic: true,
      })
    }
  }

  /**
   * 获取编辑器数据
   * @returns {{text: string, marks: *[]}}
   */
  getData() {
    return {
      text: this.text, marks: this.marks,
    }
  }

  /**
   * 重置编辑器数据
   */
  reset() {
    this.editor.setValue('')
    this.text = ''
    this.marks = []
  }

  /**
   * 插入内容
   * @param {string | object} text
   * @param {'formula' | 'field'} type
   */
  insertText(data, type) {
    const cursor = this.editor.getCursor()
    this.editor.replaceRange(type === 'field' ? JSON.stringify(data) : data, cursor)
    // 光标移动到方法的括号内
    type === 'formula' && this.moveCursor('left', 1)
    this.editor.focus()
  }

  /**
   * 移动光标
   * @param {'left' | 'right' | 'up' | 'down'} direction 方向
   * @param {number} step 步长
   */
  moveCursor(direction, step) {
    const cursor = this.editor.getCursor()
    const line = cursor.line
    const ch = cursor.ch

    // 检查是否已经在行首
    if (direction === 'left') {
      // 如果不是行首，则向左移动字符
      this.editor.setCursor({ line: line, ch: ch - step })
    } else if (direction === 'right') {
      // 如果是行首，则向右移动字符
      this.editor.setCursor({ line: line, ch: ch + step })
    } else if (direction === 'up') {
      // 如果是行首，则向上移动行
      this.editor.setCursor({ line: line - step, ch: ch })
    } else if (direction === 'down') {
      // 如果是行首，则向下移动行
      this.editor.setCursor({ line: line + step, ch: ch })
    }
  }

  // 匹配字段
  matchField(text) {
    // 使用正则表达式 /\\{[^}]+\\}/g 匹配 text 中所有形如 {内容} 的字符串，并返回匹配结果数组；若无匹配，则返回空数组 []
    const regex = /\{[^}]+\}/g
    return text.match(regex) || []
  }

  // 匹配当前行的公式
  matchWord(text) {
    // 从字符串 text 的末尾提取连续的英文字母（大小写均可），如果有匹配结果则返回该字母序列，否则返回空字符串。
    const match = text.match(/[a-zA-Z]+$/)
    return match ? match[0] : ''
  }

  // 匹配当前行的公式
  matchFormula(text) {
    if (!text) return false

    const suggestions = this.getFormulaList()
    const match = suggestions.find(o => o.name === text)
    return match
  }

  /**
   * 自定义编辑器函数根据名称自动提示
   * @param cm
   * @returns {{list: {text: string, displayText: *, tip: *}[], from: {line, ch: number}, to: {line, ch: *}}}
   */
  customHint(cm) {
    // 游标
    const cursor = cm.getCursor()
    // 当前行文本
    const currentLineText = cm.getLine(cursor.line)
    const matchWords = this.matchWord(currentLineText)
    const start = cursor.ch
    const suggestions = this.getFormulaList()
    const result = {
      list: suggestions
        .map(o => ({ text: `${o.name}()`, displayText: o.name, tip: o.desc }))
        .filter(suggestion => matchWords && suggestion.text.toLowerCase().includes(matchWords.toLowerCase()))
        .map(suggestion => {
          const text = suggestion.displayText
          const matchIndex = text
            .toLowerCase()
            .indexOf(matchWords.toLowerCase())
          suggestion.render = function(element, self, data) {
            if (matchIndex >= 0) {
              const beforeMatch = text.slice(0, matchIndex)
              const match = text.slice(matchIndex, matchIndex + matchWords.length)
              const afterMatch = text.slice(matchIndex + matchWords.length)

              const span = document.createElement('span')

              if (beforeMatch) {
                span.appendChild(document.createTextNode(beforeMatch))
              }

              const highlight = document.createElement('span')
              highlight.textContent = match
              highlight.style.fontWeight = 'bold'
              highlight.style.color = '#ff0000'
              span.appendChild(highlight)

              if (afterMatch) {
                span.appendChild(document.createTextNode(afterMatch))
              }

              element.appendChild(span)
            } else {
              element.appendChild(document.createTextNode(text))
            }
          }

          return suggestion
        }), from: { line: cursor.line, ch: start - matchWords.length }, to: { line: cursor.line, ch: start },
    }
    return result
  }
}
